1 module hip.game2d.text;
2 
3 import hip.api.data.font;
4 import hip.api.graphics.text;
5 import hip.util.data_structures;
6 
7 
8 /**
9 *   Formatting the text:
10 * Text should be formatted using the $() syntax.
11 * Currently, no formatting is support, but that syntax is reserved and in the future, it
12 * will be used as for example: $(RGB, 1.0, 1.0, 1.0) or even $(WHITE), so, basic parsing
13 * is being done for accounting how many text does really need to be rendered.
14 */
15 class HipText
16 {
17     HipTextAlign align_ = HipTextAlign.topLeft;
18     HipFont font;
19     int x, y;
20     bool wordWrap;
21     float scale = 1;
22 
23     DirtyFlagsCmp!(
24         shouldUpdateText, x, y, 
25         wordWrap, font,
26         align_,
27     ) checkDirty;
28 
29 
30     float depth = 0;
31     ///Update dynamically based on the font, the text scale and the text content
32     int width, height;
33 
34     Size bounds;
35 
36     //Line widths, containing width for each line for correctly aplying text align
37     uint[] linesWidths;
38 
39     protected string _text;
40     protected string processedText;
41     protected HipColor _color = HipColor.black;
42 
43     //Debugging?
44 
45     protected bool shouldRenderSpace = false;
46     protected bool shouldRenderLineBreak = false;
47 
48     protected HipTextStopConfig[] textConfig;
49     protected HipTextRendererVertexAPI[] vertices;
50 
51     //Caching
52     protected size_t _drawableTextCount = 0;
53     protected size_t maxDrawableTextCount = 0;
54     public bool shouldUpdateText = true;
55 
56     this(Size bounds = Size.init, bool bWordWrap = false)
57     {
58         import hip.api;
59         checkDirty.start(this);
60         this.font = cast()HipDefaultAssets.getDefaultFont();
61         linesWidths.length = 1;
62         wordWrap = bWordWrap;
63         this.bounds = bounds;
64     }
65     this(string text, int x, int y, HipFont fnt = null, Size bounds = Size.init, bool bWordWrap = false)
66     {
67         this(bounds, bWordWrap);
68         this.setPosition(x,y);
69         this.text = text;
70         if(fnt) font = fnt;
71     }
72     string text() const {return _text;}
73     size_t drawableTextCount() const {return _drawableTextCount;}
74 
75     
76     string text(string newText)
77     {
78         if(newText != _text)
79         {
80             import hip.util.string;
81             _drawableTextCount = countVertices(newText);
82             shouldUpdateText = true;
83             if(_drawableTextCount > maxDrawableTextCount)
84             {
85                 //As it is a quad, it needs to have vertices * 4
86                 vertices.length = _drawableTextCount * 4;
87                 maxDrawableTextCount = _drawableTextCount;
88             }
89             _text = newText;
90         }
91         return _text;
92     }
93 
94     void setPosition(int x, int y)
95     {
96         this.x = x;
97         this.y = y;
98     }
99 
100     HipColor color() => _color;
101     HipColor color(HipColor c) => _color = c;
102 
103     void[] getVertices()
104     {
105         checkDirty();
106         if(shouldUpdateText)
107         {
108             updateText(font);
109             checkDirty.start(this);
110         }
111         
112         return cast(void[])vertices[0..drawableTextCount * 4];
113     }
114     
115     protected void updateAlign(int lineNumber, out int displayX, out int displayY, Size bounds)
116     {
117         import hip.api.graphics.text;
118         getPositionFromAlignment(x, y, linesWidths[lineNumber], height, align_, displayX, displayY, bounds);
119     }
120     
121 
122     public void getSize(out int width, out int height)
123     {
124         if(processedText == null)
125             HipTextStopConfig.parseText(_text, processedText, textConfig);
126         font.calculateTextBounds(processedText, linesWidths, width, height, bounds.width);
127         this.width = width;
128         this.height = height;
129     }
130     public void setAlign(HipTextAlign align_)
131     {
132         this.align_ = align_;
133     }
134 
135     package void updateText(IHipFont font)
136     {
137         import hip.api.graphics.text;
138         HipTextStopConfig.parseText(_text, processedText, textConfig);
139         int vI = 0; //vertex buffer index
140 
141         vI = putTextVertices(font, vertices[vI..$], processedText, x, y, depth, scale, align_, bounds, wordWrap, shouldRenderSpace);
142         shouldUpdateText = true;
143     }
144 
145     void draw()
146     {
147         import hip.api.graphics.g2d.g2d_binding;
148         setTextColor(color);
149         drawTextVertices(getVertices, font);
150     }
151 }
152 
153 
154 /**
155 *   The text stop config defines how this text will behave a
156 */
157 package struct HipTextStopConfig
158 {
159     import hip.api.graphics.color;
160     int startIndex;
161     HipColorf color;
162 
163     //This is just a plan, not supported right now
164     public static enum Tokens
165     {
166         alignh = "alignh",
167         alignv = "alignv",
168         rgb = "rgb",
169         color = "color",
170         bold = "bold",
171         italic = "italic",
172         red = "red",
173         green = "green",
174         blue = "blue",
175     }
176 
177     private static HipTextStopConfig parseToken(in string text, size_t indexToParse, out size_t continueIndex)
178     {
179         import hip.util.conv;
180         import hip.util.string;
181         import hip.util.algorithm;
182         int endIndex = text[indexToParse..$].indexOf(")"); //Won't support parenthesis between them.
183         assert(endIndex != -1, "Missing ending parenthesis on string at HipTextStopConfig formatting ");
184         continueIndex = endIndex+indexToParse;
185 
186 
187         auto range = splitRange(text[indexToParse..endIndex], ",");
188         string token = range.front;
189         range.popFront();
190 
191         switch(token)
192         {
193             case "rgb":
194             {
195                 HipColorf c = HipColorf(0, 0, 0, 1.0);
196                 range.map((string x) => x.trim.to!float).put(&c.r, &c.g, &c.b);
197                 return HipTextStopConfig(cast(int)indexToParse, c);
198             }
199             default: break;
200         }
201         return HipTextStopConfig(cast(int)indexToParse, cast()HipColorf.white);
202     }
203 
204 
205     static void parseText(string text, out string parsedText, ref HipTextStopConfig[] config)
206     {
207         parsedText = text;
208         // size_t indexConfig = 0;
209         // size_t lastParseIndex = 0;
210         // string parsingText;
211         // for(size_t i = 0; i < text.length; i++)
212         // {
213         //     if(i+1 < text.length && text[i] == '$' && text[i+1] == '(') //Found something to parse
214         //     {
215         //         parsingText~= text[lastParseIndex..i-1];
216         //         HipTextStopConfig cfg = parseToken(text, i+1, i); //Update i
217         //         lastParseIndex = i;
218         //         if(indexConfig >= config.length)
219         //             config.length++;
220         //         config[indexConfig++] = cfg;
221         //     }
222         // }
223         // //!FIXME: This allocated on each frame. It should both be used a @nogc operation (String) or it should 
224         // //!find a way to create a range to be used instead of a string.
225         // if(lastParseIndex == 0)
226         // {
227         //     parsedText = text;
228         //     return;
229         // }
230         // parsedText = parsingText ~ text[lastParseIndex..$];
231     }
232 
233 }
234 
235 
236 
237 private size_t countVertices(string str)
238 {
239     size_t i = 0;
240     foreach(ch; str)
241     {
242         if(ch != ' ' && ch != '\n')
243             i++;
244     }
245     return i;
246 }